Skip to content

feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453

Open
TheodoreSpeaks wants to merge 10 commits into
stagingfrom
feat/custom-block-settings
Open

feat(custom-block): move management to enterprise settings; derive inputs live from deployed start#5453
TheodoreSpeaks wants to merge 10 commits into
stagingfrom
feat/custom-block-settings

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Move custom-block creation/editing out of the workflow Deploy modal into a dedicated Enterprise settings page (pick workspace → deployed workflow → per-input placeholders → curated outputs → icon)
  • Store only per-input placeholders in custom_block.inputs; always derive the input field set (name/type) live from the deployed Start, so an input removed from the source can't linger
  • Icon chain: uploaded icon → org whitelabel logo → default glyph; fix image-icon sizing in canvas/toolbar/command palette
  • Harden lifecycle: deleting a custom block no longer corrupts consuming workflows (dropped like a removed block); deleted inputs are never passed to the child; object/array inputs decode correctly (no double-encoding)

Type of Change

  • New feature + bug fixes

Testing

Tested manually against a live deployed workflow. Added unit tests: serializer drops a deleted custom block + its edges; handler remap drops non-existent fields and decodes json inputs. bun run lint, check:api-validation:strict, and check:migrations origin/staging all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 7, 2026 3:16am

Request Review

@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches workflow serialization, child-workflow execution, and org-wide block overlays—incorrect input mapping or drop logic could break existing workflows using custom blocks, though new tests cover the main edge cases.

Overview
Moves custom block publish/edit out of the workflow Deploy modal into a new Enterprise Custom blocks settings section (gated by NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED), with list + detail flows for workspace/workflow selection, icons, curated outputs, and per-Start-input placeholders.

Inputs are stored only as placeholder overrides in custom_block.inputs (migration 0256); name/type/description always come from the deployed Start. Publish/update APIs accept inputs; list responses add workflowName / workspaceName.

Runtime/UI behavior: disabled blocks stay resolvable on canvas (hideFromToolbar) but disappear from the palette; icons fall back uploaded → whitelabel logo → glyph with sizing fixes. Serializer drops unresolvable custom blocks and stale input values when the config declares inputs; workflow handler decodes object/array JSON and fails clearly when the source workflow is undeployed.

Query invalidation broadens to all custom-block lists after mutations.

Reviewed by Cursor Bugbot for commit b07b062. Bugbot is set up for automated code reviews on this repo. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/hooks/queries/custom-blocks.ts Outdated
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
notDeployed ||
iconUpload.isUploading ||
deployed.isLoading ||
(deployedLoaded && visibleOutputs.length === 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy blocks cannot update

Medium Severity

The detail view requires at least one curated output to enable Save and rejects empty exposedOutputs. Blocks published with no outputs (previously “all outputs / result”) keep exposedOutputs: [] and cannot be edited or updated in the new settings UI.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 34108b0. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves custom-block creation/editing from the Deploy modal into a dedicated Enterprise settings page, changes custom_block.inputs to store only per-input placeholders (field set is always derived live from the deployed Start), adds whitelabel logo as a fallback icon, and hardens the lifecycle so deleting a block or its inputs no longer corrupts consuming workflows.

  • New Enterprise settings page (ee/custom-blocks/) provides a full CRUD UI for custom blocks: workspace/workflow picker, per-input placeholder overrides, curated output selection, and icon upload with the whitelabel-logo fallback chain.
  • Serializer hardening: unresolvable custom blocks are now silently dropped (with their edges) during both serialize and deserialize, preventing the Invalid block type crash; deleted inputs are filtered out of inputMapping when the block config declares its inputs; JSON-valued object/array fields are decoded before being passed to the child.
  • DB migration 0255 adds a nullable json inputs column to custom_block to persist per-input placeholder overrides.

Confidence Score: 4/5

The serializer, handler, and operations changes are well-structured and covered by new tests; the main risk is the still-open cross-workspace cache collision in useCustomBlocks (flagged in a prior review and not yet addressed) which could serve stale blocks during a workspace switch.

The lifecycle hardening (dropped blocks, filtered inputs, JSON decode) is correct and well-tested. The new settings UI wires up inputs/outputs cleanly with live deployment derivation. The pre-existing useCustomBlocks query key omits workspaceId while the queryFn still scopes by workspace — a user who switches between two org workspaces within the stale window sees the first workspace's custom blocks in the palette of the second.

apps/sim/hooks/queries/custom-blocks.ts — the query key needs a workspaceId segment to prevent cross-workspace cache collisions.

Important Files Changed

Filename Overview
apps/sim/hooks/queries/custom-blocks.ts Mutation invalidations broadened from customBlockKeys.list(workspaceId) to customBlockKeys.lists(), but the useCustomBlocks query key remains customBlockKeys.lists() without a workspaceId segment — the queryFn still scopes by workspaceId, creating a cross-workspace cache collision (flagged in previous review).
apps/sim/executor/handlers/workflow/workflow-handler.ts Adds pre-try deployment check for custom blocks (intentionally outside the catch sanitizer), skips the redundant deployed-check for custom blocks inside the try, adds context switching to source-owner identity/env, and sanitizes custom-block errors in the catch path.
apps/sim/ee/custom-blocks/components/custom-block-detail.tsx New 681-line settings detail panel for creating/editing custom blocks; derives inputs live from deployed Start, stores only placeholder overrides, and manages icon upload with whitelabel fallback.
apps/sim/serializer/index.ts Adds graceful handling for unresolvable custom block types during both serialize and deserialize — drops the block and its edges instead of throwing; also filters out deleted inputs from inputMapping when the block config declares its inputs.
apps/sim/lib/workflows/custom-blocks/operations.ts getCustomBlockRowsForOrg now returns disabled blocks too (with enabled flag) so placed instances survive serialization; applyInputPlaceholders merges stored placeholder overrides onto live-derived input fields; listCustomBlocksWithInputs joins workflow/workspace for display names.
apps/sim/serializer/custom-block-lifecycle.test.ts New test file covering the two lifecycle bug fixes: dropped inputs don't leak into inputMapping, and deleted custom blocks are safely dropped (not fatal) during serialize and deserialize.
apps/sim/blocks/custom/build-config.ts Exports RESERVED_PARAMS and CustomBlockInput interface for shared use; adds hideFromToolbar option to buildCustomBlockConfig; adds placeholder to sub-block config; narrows isCustomBlockType return to type predicate.
packages/db/migrations/0255_custom_block_inputs.sql Adds nullable json column inputs to custom_block; backward-compatible (existing rows get NULL, handled by applyInputPlaceholders).
apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx Fetches whitelabel settings as icon fallback; now passes all blocks (not just enabled) to hydrateClientCustomBlocks with hideFromToolbar for disabled ones; two-phase hydration (data then whitelabel) may cause a brief flash of the default icon.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Loader as CustomBlocksLoader
    participant Registry as Block Registry
    participant Executor as WorkflowBlockHandler

    Admin->>API: POST /api/custom-blocks (workflowId, name, inputs[], outputs[])
    API->>DB: "INSERT with inputs=placeholders[], outputs=[]"
    DB-->>API: new row
    API-->>Admin: CustomBlockWithInputs

    Note over Loader,Registry: On workspace mount
    Loader->>API: "GET /api/custom-blocks?workspaceId=..."
    API->>DB: SELECT all rows (enabled + disabled)
    DB-->>API: rows[]
    API-->>Loader: CustomBlock[] (inputFields derived live from deployed Start)
    Loader->>Registry: "hydrateClientCustomBlocks(all, hideFromToolbar=!enabled)"

    Note over Executor: At run-time
    Executor->>Executor: isCustomBlock? → checkChildDeployment (outside try)
    Executor->>Executor: remapCustomBlockInputKeys (id→name, decode JSON values)
    Executor->>Executor: execute child under source owner's identity/env
    Executor->>Executor: projectCustomBlockOutput (curated outputs only)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Loader as CustomBlocksLoader
    participant Registry as Block Registry
    participant Executor as WorkflowBlockHandler

    Admin->>API: POST /api/custom-blocks (workflowId, name, inputs[], outputs[])
    API->>DB: "INSERT with inputs=placeholders[], outputs=[]"
    DB-->>API: new row
    API-->>Admin: CustomBlockWithInputs

    Note over Loader,Registry: On workspace mount
    Loader->>API: "GET /api/custom-blocks?workspaceId=..."
    API->>DB: SELECT all rows (enabled + disabled)
    DB-->>API: rows[]
    API-->>Loader: CustomBlock[] (inputFields derived live from deployed Start)
    Loader->>Registry: "hydrateClientCustomBlocks(all, hideFromToolbar=!enabled)"

    Note over Executor: At run-time
    Executor->>Executor: isCustomBlock? → checkChildDeployment (outside try)
    Executor->>Executor: remapCustomBlockInputKeys (id→name, decode JSON values)
    Executor->>Executor: execute child under source owner's identity/env
    Executor->>Executor: projectCustomBlockOutput (curated outputs only)
Loading

Reviews (3): Last reviewed commit: "fix(custom-block): keep disabled blocks ..." | Re-trigger Greptile

Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves custom-block management out of the Deploy modal into a dedicated Enterprise Settings page, stores only per-input placeholder hints in the DB (deriving the full input field set live from the deployed Start block so removed inputs can never linger), hardens the lifecycle so deleted custom blocks are dropped gracefully instead of corrupting workflows, and fixes image-icon sizing across the canvas, toolbar, and command palette.

  • New Enterprise Settings page (custom-block-detail.tsx, custom-blocks.tsx): create/edit/delete custom blocks; input fields always reflect the latest deployment; only authored data (placeholders) is persisted.
  • Lifecycle hardening: the serializer now drops unresolvable custom blocks and their edges on both serialize/deserialize; extractBlockParams filters deleted inputs for blocks whose config declares its input set; remapCustomBlockInputKeys JSON-decodes object/array inputs and drops keys with no matching child field.
  • DB schema: a nullable inputs JSON column is added to custom_block via a non-destructive migration; the inputs column stores Array<{id, placeholder?}> only.

Confidence Score: 3/5

The lifecycle and serializer changes are well-tested and correct, but the query-key change introduces a cross-workspace cache issue that should be resolved before merging.

The mutation invalidations rightly use customBlockKeys.lists() (the prefix), but the query in useCustomBlocksQuery was also switched to that same prefix key, dropping workspaceId from the cache entry. fetchCustomBlocks still passes workspaceId to the API, so two workspaces from different organisations share one React Query cache slot. During the 60-second stale window, a user who belongs to multiple org workspaces would see stale data from the wrong org — and the project's enforced key-fetch-arg-drift CI check exists specifically to catch this. Everything else in the PR — the DB migration, serializer hardening, handler remap, icon sizing, and the new settings UI — looks solid.

apps/sim/hooks/queries/custom-blocks.ts — the queryKey for useCustomBlocksQuery must include workspaceId

Important Files Changed

Filename Overview
apps/sim/hooks/queries/custom-blocks.ts Query key changed from workspace-scoped customBlockKeys.list(workspaceId) to prefix-level customBlockKeys.lists(), causing cross-workspace cache collisions when the same user is a member of multiple organisations' workspaces
apps/sim/executor/handlers/workflow/workflow-handler.ts Custom block deployment check moved outside the inner try block (intentional: prevents generic sanitiser from masking the human-readable error message); object/array inputs are now JSON-decoded before passing to the child; checkChildDeployment is called exactly once per path
apps/sim/serializer/index.ts Deleted custom blocks are now gracefully dropped (with their edges) on both serialize and deserialize instead of throwing Invalid block type; deleted inputs on live blocks with declared configs are also filtered out
apps/sim/lib/workflows/custom-blocks/operations.ts Input fields now derived live from the deployed Start block with stored placeholders merged in; workflowName and workspaceName joined from related tables for display
apps/sim/ee/custom-blocks/components/custom-block-detail.tsx New 659-line settings UI for creating/editing custom blocks; derives inputs from deployed Start state, authors only per-field placeholders, and guards unsaved navigation
packages/db/schema.ts Adds nullable inputs JSON column to custom_block for per-field placeholder storage; migration is a single non-destructive ALTER TABLE ADD COLUMN
apps/sim/blocks/custom/custom-block-icon.tsx Image icon now applies size-full last in cn() so it fills a tile container; !important modifiers removed from toolbar/command-palette consumers intentionally so size-full wins for image icons while non-image glyphs retain the small size; fallback chain added (uploaded → whitelabel logo → default glyph)
apps/sim/serializer/custom-block-lifecycle.test.ts New test file covering both bugs: deleted input not leaking into inputMapping, and deleted custom block being dropped gracefully on serialize/deserialize

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Start as Deployed Start Block
    participant Consumer as Consumer Workflow

    Admin->>API: "POST publish {workflowId, name, inputs:[{id,placeholder}], exposedOutputs}"
    API->>DB: "INSERT custom_block (inputs=placeholders only)"
    API->>Start: deriveInputFields(workflowId)
    Start-->>API: "[{id,name,type}...]"
    API-->>Admin: block with live inputFields (placeholders merged)

    Admin->>API: "PATCH update {inputs, exposedOutputs}"
    API->>DB: UPDATE inputs column

    Consumer->>API: GET /api/custom-blocks
    API->>DB: SELECT custom_block JOIN workflow JOIN workspace
    API->>Start: deriveInputFields per enabled block
    Start-->>API: live field set
    API-->>Consumer: "inputFields = live fields + stored placeholders merged"

    Consumer->>Consumer: serialize workflow
    Note over Consumer: isCustomBlockType + !getBlock → drop block + edges
    Consumer->>Consumer: remapCustomBlockInputKeys (drop stale, decode JSON)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Admin as Admin (Settings UI)
    participant API as /api/custom-blocks
    participant DB as custom_block (DB)
    participant Start as Deployed Start Block
    participant Consumer as Consumer Workflow

    Admin->>API: "POST publish {workflowId, name, inputs:[{id,placeholder}], exposedOutputs}"
    API->>DB: "INSERT custom_block (inputs=placeholders only)"
    API->>Start: deriveInputFields(workflowId)
    Start-->>API: "[{id,name,type}...]"
    API-->>Admin: block with live inputFields (placeholders merged)

    Admin->>API: "PATCH update {inputs, exposedOutputs}"
    API->>DB: UPDATE inputs column

    Consumer->>API: GET /api/custom-blocks
    API->>DB: SELECT custom_block JOIN workflow JOIN workspace
    API->>Start: deriveInputFields per enabled block
    Start-->>API: live field set
    API-->>Consumer: "inputFields = live fields + stored placeholders merged"

    Consumer->>Consumer: serialize workflow
    Note over Consumer: isCustomBlockType + !getBlock → drop block + edges
    Consumer->>Consumer: remapCustomBlockInputKeys (drop stale, decode JSON)
Loading

Comments Outside Diff (1)

  1. apps/sim/hooks/queries/custom-blocks.ts, line 35-41 (link)

    P1 workspaceId in queryFn but absent from queryKey

    fetchCustomBlocks(workspaceId, ...) is scoped by workspaceId, but the key is now customBlockKeys.lists() (no workspace segment). Two calls with different workspaceId values — e.g., a user who belongs to workspaces from two different organisations — share one cache entry: the second workspace's query returns the first workspace's custom blocks until the 60-second stale window expires. The project's sim-queries.md rule ("Every identifier the queryFn forwards into the fetch MUST appear in the queryKey") and the enforced key-fetch-arg-drift CI check both forbid this pattern.

    The mutation hooks correctly use customBlockKeys.lists() for invalidation (that prefix covers all workspace-specific entries), but the query key must be the workspace-specific form customBlockKeys.list(workspaceId).

Reviews (2): Last reviewed commit: "fix(custom-block): key custom-blocks que..." | Re-trigger Greptile

Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx Outdated
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/lib/workflows/custom-blocks/operations.ts
Comment thread apps/sim/serializer/index.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

…settings

# Conflicts:
#	packages/db/migrations/meta/0255_snapshot.json
#	packages/db/migrations/meta/_journal.json

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5b73abb. Configure here.

Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
Comment thread apps/sim/ee/custom-blocks/components/custom-block-detail.tsx
…aceholders; reseed edit form after async load
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant